Python 解包 - unpacking
Python 解包 - unpacking
参考文档:
一问告诉你什么是python解包(Unpacking)_*range(4),r_LoveMIss-Y的博客-CSDN博客
解包(Unpacking),顾名思义,就是将容器里面的元素逐个取出来。
注意,这个概念,Java 中是没有的。
Python 中解包操作,在某些场景下是自动完成的,在某些场景下需要借助操作符:*
操作符和 **
操作符,
-
*
:迭代器解包操作,也称之为容器/序列拆分操作符 -
**
:字典解包操作,也称之为映射拆分操作。作为关键字参数传递给函数。
使用 *
和 **
的解包的好处是能节省代码量,使得代码看起来更优雅。
注意:任何可迭代对象都支持解包,可迭代对象包括元组、字典、集合、字符串、生成器等实现了 __next__
方法的一切对象。
变量赋值过程中的自动解包
容器/序列的自动解包
注意,字典解包后,只会把字典的 key 取出来,val 则丢掉了。我们在很多地方都注意到了这个现象
# 两个变量的类型都是 str
char_a, char_b = "ab"
print(char_a, type(char_a), char_b, type(char_b))
# 两个变量是int类型
list_ele_a, list_ele_b = [1212, 1313]
print(list_ele_a, type(list_ele_a), list_ele_b, type(list_ele_b))
# 两个变量是字符串类型
tuple_ele_a, tuple_ele_b = ("bbbb", "cccc")
print(tuple_ele_a, type(tuple_ele_a), tuple_ele_b, type(tuple_ele_b))
# 元组的隐式声明
tuple_ele_a, tuple_ele_b = "bbbb", "cccc"
print(tuple_ele_a, type(tuple_ele_a), tuple_ele_b, type(tuple_ele_b))
# 变量都是int类型,且元素并不是按照 1414, 1515, 1616, 1717赋值,而是任意顺序
set_ele_a, set_ele_b, set_ele_c, set_ele_d = {1414, 1515, 1616, 1717}
print(set_ele_a, type(set_ele_a), set_ele_b, type(set_ele_b), set_ele_c, type(set_ele_c), set_ele_d, type(set_ele_d))
# 字典的自动解包,只获取了key,丢掉了val
# 变量都是int类型,且元素按照 1414, 1515, 1616, 1717赋值
dict_ele_a, dict_ele_b, dict_ele_c, dict_ele_d = {1818: "aa", 1919: "bb", 2020: "cc", 2121: "dd"}
print(dict_ele_a, type(dict_ele_a), dict_ele_b, type(dict_ele_b), dict_ele_c, type(dict_ele_c), dict_ele_d,
type(dict_ele_d))
输出:
a <class 'str'> b <class 'str'>
1212 <class 'int'> 1313 <class 'int'>
bbbb <class 'str'> cccc <class 'str'>
bbbb <class 'str'> cccc <class 'str'>
1616 <class 'int'> 1515 <class 'int'> 1717 <class 'int'> 1414 <class 'int'>
1818 <class 'int'> 1919 <class 'int'> 2020 <class 'int'> 2121 <class 'int'>
如果可迭代对象中元素的个数大于接收变量的个数怎么办? 在其中一个变量名前加上 *
,注意只能给一个变量加,给两个加会报错
最终的效果就是带 *
号的变量变成一个列表(不论等号右边是什么类型,带星号的都会变成列表),且带 *
号的变量包含的元素中的值在等号右侧的相对位置,跟变量在等号左侧的变量列表所处的相对位置相同
a, *b, c = [1, 2, 3, 4, 5]
# 1 [2, 3, 4] 5
print(a, b, c)
a, *b, c = (1, 2, 3, 4, 5)
# b依然是一个列表
# 1 [2, 3, 4] 5
print(a, b, c)
a, *b, c = {1, 2, 3, 4, 5}
# b依然是一个列表
# 1 [2, 3, 4] 5
print(a, b, c)
输出:
1 [2, 3, 4] 5
1 [2, 3, 4] 5
1 [2, 3, 4] 5
如果可迭代对象只包含一个元素,我只想要这个元素,而不是想要这个可迭代对象对象,在变量后面加一个逗号即可
# a 为 int 类型
a, = [1]
print(a, type(a))
# a 为 list 类型
a = [1]
print(a, type(a))
输出:
1 <class 'int'>
[1] <class 'list'>
多变量的赋值
多变量的赋值与交换本质上也是自动解包,因为等号右边的会被看作是元组对象
int_a, int_b, int_c = 1, 2, 3
print(int_a, type(int_a), int_b, type(int_b), int_c, type(int_c))
# 实际上等价于
int_a, int_b, int_c = (1, 2, 3)
print(int_a, type(int_a), int_b, type(int_b), int_c, type(int_c))
输出:
1 <class 'int'> 2 <class 'int'> 3 <class 'int'>
1 <class 'int'> 2 <class 'int'> 3 <class 'int'>
多变量的交换
轻松实现不需要声明第三个变量的两值交换,牛逼
int_a, int_b, int_c = int_c, int_a, int_b,
# 3 1 2
print(int_a, int_b, int_c)
int_a, int_b, int_c = int_c - int_a, int_c + int_b, int_a + int_b,
# -1 3 4
print(int_a, int_b, int_c)
输出:
3 1 2
-1 3 4
利用这个特性,我们可以非常方便地计算斐波那契数列
# 计算斐波那契数列
a = 0
b = 1
for i in range(10):
a, b = b, a + b
# 输出 89
print(b)
输出:
89
表达式中的解包
我们在学习我们在学习各种容器的各种 API,本质上是把容器当成一个整体来操作,这种操作是无法跨容器类型的,现在有了解包操作,我们可以直接对容器中的元素进行操作,自然也就不存在容器类型的限制了。通过解包操作,我们可以轻松实现各种不同类型的容器中的元素的苹姐,这在没有解包操作的时候,都是非常繁琐的。
tuple_1 = *range(4), 4
# (0, 1, 2, 3, 4)
print(tuple_1)
# range直接解包无法用变量承接,加一个, 表示用元组承接
tuple_2 = *range(4),
print(tuple_2)
# [0, 1, 2, 3, 4]
print([*range(4), 4])
# {0, 1, 2, 3, 4}
print({*range(4), 4})
# 这个操作实际上相当于拼接了两个字典
# **对字典的解包,只能用在{}内部,好像是这样
# {'x': 1, 'y': 2, 'z': 3}
print({'x': 1, **{'y': 2, 'z': 3}})
# 例如,相当于啥都没做
dict_ele_a, dict_ele_b = {**{"a": 1, "b": 3}}
print(dict_ele_a, dict_ele_b)
# list的拼接
list_raw = ["a", "b", "c"]
list_compose = [*range(4), *list_raw]
# 输出 [0, 1, 2, 3, 'a', 'b', 'c']
print(list_compose)
tuple_raw = ("1", "2", "3")
tuple_compose = (*tuple_raw, *range(4))
# 输出 ('1', '2', '3', 0, 1, 2, 3)
print(tuple_compose)
set_raw = {"1", "2", "3"}
set_compose = {*set_raw, *range(4)}
# 输出 {0, 1, 2, 3, '1', '3', '2'}
print(set_compose)
# 两个字典的拼接
dict_raw = {'x': 1, 'y': 2, 'z': 3}
dict_compose = {"a": "bc", **dict_raw}
# 输出 {'a': 'bc', 'x': 1, 'y': 2, 'z': 3}
print(dict_compose)
输出:
(0, 1, 2, 3, 4)
(0, 1, 2, 3)
[0, 1, 2, 3, 4]
{0, 1, 2, 3, 4}
{'x': 1, 'y': 2, 'z': 3}
a b
[0, 1, 2, 3, 'a', 'b', 'c']
('1', '2', '3', 0, 1, 2, 3)
{0, 1, 2, 3, '2', '1', '3'}
{'a': 'bc', 'x': 1, 'y': 2, 'z': 3}
看起来好像没啥了不起,但是实际上, list 类型无法与 range 对象相加,你必须先将 range() 强制转换为 list 对象才能做 +
操作,因此 *
的解包操作实际上还是很方便的
我们可以看到,**
对字典的解包,只能用在 {}
内部使用,好像是这样
以上好像看不出什么,现在才是解包的妙用,通过解包,我们可以将元组和集合中的元素合并成一个列表。
tuple_2_use = ("1", "2", "3",)
set_2_use = {"a", "b", "c"}
list_result = [*tuple_2_use, *set_2_use]
# 输出 ['1', '2', '3', 'c', 'a', 'b']
print(list_result)
输出:
['1', '2', '3', 'c', 'b', 'a']
函数调用过程中的解包
需要用到前面提到的 *
和 **
这两个符号,函数被调用的时候,使用星号 *
解包一个可迭代对象作为函数的参数,作为位置参数传递给函数。字典对象,可以使用 **
,解包之后将作为关键字参数传递给函数。
使用
*
和**
的解包的好处是能节省代码量,使得代码看起来更优雅
这个不要跟方法声明语句中的参数的
*
搞混,参考 《函数.md》中的参数名以*开头
小节
Python3.5,也就是 PEP 448 对解包操作做了进一步扩展, 在 3.5 之前的版本,函数调用时,一个函数中解包操作只允许一个
*
和 一个**
。从 3.5 开始,在函数调用中,可以有任意多个解包操作。
先定义两个函数:
def func_test_unpacking(a, b, c):
print(a, b, c)
def func_test_unpacking_over(a, *b, c):
print(a, b, c)
然后先对可迭代对象进行解包,以解包后的结果为参数调用函数
func_test_unpacking(*"abc")
func_test_unpacking(*["aaa", "bbb", "ccc"])
func_test_unpacking(*(1, 1, 1))
func_test_unpacking(*{1, "aa", "bbb"})
输出:
a b c
aaa bbb ccc
1 1 1
1 aa bbb
如果可迭代对象的个数比方法的参数多,函数也可以用 *
放到变量前面,对应上面的 func_test_unpacking_over
方法,这样,这个方法形参就会将多的参数汇总为一个元组,此时,后面的形参就必须以关键字参数的格式传参。
参考 《函数.md》中的
参数名以*开头
小节
# 输出 a ('b', 'c', 'd', 'e', 'f', 'g') &&
# a 为 a
# b 为内容为 ('b', 'c', 'd', 'e', 'f', 'g') 的元组
# c 为 && 为关键字参数
func_test_unpacking_over(*"abcdefg", c="&&")
输出:
a ('b', 'c', 'd', 'e', 'f', 'g') &&
字典有点特殊
用 *
来解包字典的时候,会跟前面一样将 key 作为结果传入函数,val 丢弃掉,用 **
来解包字典的时候,会以关键字参数的形式传入参数,其中字典的 key 就是对应参数名,val 就是参数值
注意字典不能包含比方法参数多的 key,不然会报错,能不能少得看少的那个关键字参数有没有默认值,有的话就可以省略
# 用 * 来解包字典的时候,会跟前面一样将key作为结果传入函数
# 输出 1 2 3
func_test_unpacking(*{1: "c", "2": "b", 3: "c"})
# 用 ** 来解包字典的时候,会以关键字参数的形式传入参数,其中字典的key就是对应参数名,val就是参数值
# 注意字典不能包含比方法参数多的key,不然会报错,能不能少得看少的那个关键字参数有没有默认值,有的话就可以省略
# 输出 10 11 12
func_test_unpacking(**{"a": 10, "b": 11, "c": 12})
输出:
1 2 3
10 11 12